Skip to content

Add ExternalAudioSource for publishing app audio as an independent track - #1084

Draft
hiroshihorie wants to merge 6 commits into
mainfrom
hiroshi/external-audio-source
Draft

Add ExternalAudioSource for publishing app audio as an independent track#1084
hiroshihorie wants to merge 6 commits into
mainfrom
hiroshi/external-audio-source

Conversation

@hiroshihorie

Copy link
Copy Markdown
Member

Adds ExternalAudioSource, a pushable audio source the app feeds PCM into, so app or screen-share audio can be published as an independent track instead of being mixed into the microphone track inside the engine.

Built on webrtc-sdk/webrtc#282, which adds the native RTCExternalAudioSource and keeps ADM audio out of externally fed send streams. Draft until a LiveKitWebRTC release containing that lands and the pins here can be bumped. CI is expected to fail against 144.7559.11, which lacks the new API and the updated ADM delegate signature.

API

  • ExternalAudioSource: push AVAudioPCMBuffer or CMSampleBuffer, foreign formats are converted via a cached AudioConverter. Buffered pacing with overflow rejection, bufferedDurationMs, clearBuffer().
  • LocalAudioTrack.createTrack(externalSource:): publishes as .screenShareAudio, and startCapture() short-circuits so the ADM recording path never starts.
  • ScreenShareCaptureOptions.appAudioPublishMode: .mix (default, existing behavior) or .separateTrack. With .separateTrack, screen share publishes app audio as its own track and unpublishing the video takes the audio track with it.
  • Capturer routing: BroadcastScreenCapturer (iOS) and MacOSScreenCapturer feed the source directly. In .separateTrack mode a missing sink drops audio with a single warning instead of silently falling back into the mic mix. iOS in-app capture has no audio path, so track prep bails with a warning instead of publishing a permanently silent track.

Commit guide

  • 0babffe6 the feature itself (wrapper, track creation, options, capturer routing, lifecycle, E2E test)
  • d794daa6 unpublish all .screenShareAudio publications, not just the first match
  • 6518a07b fail track prep when the capturer has no audio path, tag the E2E test .e2e, assert the ADM recording path never started
  • 1bad4614 never route separate-track app audio into the mic mixer, log app-audio publish failures and let the share continue
  • 8795a447 skip the publish frame-wait gate from Ensure audio frame when publishing #690 for externally fed tracks. The watcher observes the ADM capture path, which external sources bypass by design, so the publish always timed out
  • bc739c86 adapt to the new ADM willEnableEngine delegate signature (webrtc-sdk#275). Required by any framework built past 144.7559.11, rides with the pin bump

Testing

  • ExternalAudioSourceTests 4/4 against livekit-server --dev, including publish to a remote participant with the ADM recording path asserted never started
  • Verified on device (macOS): screen share via set(source:) publishes video plus an independent screen_share_audio track, remote side gets separate volume control per track, stop unpublishes both
  • Known follow-up: webrtc-sdk#265 made the engine's mic permission check passive, and the request side is not implemented in the SDK yet. With TCC undetermined, mic publish fails with no prompt unless the app calls ensureDeviceAccess first. Needs a decision whether the SDK requests automatically in the publish path

Merge gates

Wraps the new RTCExternalAudioSource from the webrtc-sdk fork: an
audio source the app pushes AVAudioPCMBuffer/CMSampleBuffer audio
into, bypassing the AudioDeviceModule and the mic mixer entirely.
LocalAudioTrack.createTrack(externalSource:) publishes it with the
screenShareAudio source, and buffers are converted to the declared
format via a cached AudioConverter.

ScreenShareCaptureOptions gains appAudioPublishMode: .mix keeps the
current behavior of summing app audio into the microphone track,
.separateTrack routes captured app audio (ReplayKit broadcast on iOS,
ScreenCaptureKit on macOS) to an independent track that follows the
screen-share video lifecycle, including unpublish on out-of-band
stops.

Requires a webrtc-xcframework build containing LKRTCExternalAudioSource;
the version pin bump lands separately once that release exists.
A missed cleanup or double publish must not leave stale screen-share
audio tracks behind on the next stop. Also route the stop path through
unpublishAppAudioTrackIfNeeded instead of duplicating the lookup.
On iOS with useBroadcastExtension false the capturer is the in-app
capturer, which has no audio handling. The cast to
BroadcastScreenCapturer silently returned nil and the separate track
was published anyway, permanently silent. Bail with a warning instead,
on both platforms.

Also tag the E2E test .e2e to match sibling suites and assert the ADM
recording path never started, which is the guarantee the test exists
to prove.
webrtc-sdk PR 275 added isVoiceProcessingEnabled to the delegate
method, so frameworks built past 144.7559.11 fail protocol conformance
without this. The parameter is ignored for now, exposing it through
AudioEngineObserver belongs to the framework version bump.
With appAudioPublishMode == .separateTrack, a missing appAudioSink
(the audio track failed to publish or was unpublished mid-share) fell
back to AudioManager.shared.mixer, silently mixing app audio into the
microphone track. And when the mic is not published the mixer has no
converter, spamming a warning per buffer. Drop the buffers with a
single warning instead, in both capturers.

Also log an app-audio publish failure and let the screen share
continue instead of unwinding after the video track already published,
which is exactly the state that leaves the weak sink dangling.
The wait added in #690 observes the ADM capture path via an audio
renderer, which external sources bypass by design, so publishing an
external track always timed out after the rebase picked the gate up.
Frames also only flow once the app pushes audio, so there is nothing
to deterministically wait for. The server had already accepted the
track, the timeout then tore it down and the capturer's weak sink
died, which is how this surfaced.
@github-actions

Copy link
Copy Markdown

⚠️ This PR does not contain any files in the .changes directory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant